#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import time
import logging
import asyncio
import platform
import ctypes
import shutil
from datetime import datetime, timedelta
from pathlib import Path
from typing import List, Set, Tuple, Optional

logger = logging.getLogger(__name__)

class FileCleaner:
    """文件清理服务，定时清理指定目录下的旧文件"""

    def __init__(self, 
                 output_dir: str = "output", 
                 temp_dir: str = "temp", 
                 uploads_dir: str = "uploads",
                 max_age_minutes: int = 30,
                 check_interval_seconds: int = 60):
        """
        初始化文件清理服务
        
        Args:
            output_dir: 输出目录
            temp_dir: 临时目录
            uploads_dir: 上传目录
            max_age_minutes: 文件最大保留时间（分钟）
            check_interval_seconds: 检查间隔时间（秒）
        """
        self.output_dir = Path(output_dir)
        self.temp_dir = Path(temp_dir)
        self.uploads_dir = Path(uploads_dir)
        self.max_age_minutes = max_age_minutes
        self.check_interval_seconds = check_interval_seconds
        self.running = False
        self.cleanup_task = None
        # 检测操作系统
        self.is_windows = platform.system().lower() == 'windows'

    async def start(self):
        """启动清理服务"""
        if self.running:
            logger.debug("文件清理服务已经在运行")
            return
        
        self.running = True
        
        # 立即执行一次清理操作
        logger.info(f"文件清理服务启动，立即执行首次清理...")
        try:
            await self.cleanup_old_files()
            logger.info(f"首次清理完成，将定期清理超过 {self.max_age_minutes} 分钟的文件")
        except Exception as e:
            logger.error(f"首次清理操作失败: {str(e)}")
        
        # 启动定期清理任务
        self.cleanup_task = asyncio.create_task(self._cleanup_loop())
        logger.info(f"文件清理服务已启动定时任务，间隔: {self.check_interval_seconds}秒")

    async def stop(self):
        """停止清理服务"""
        if not self.running:
            logger.debug("文件清理服务未在运行")
            return
        
        self.running = False
        if self.cleanup_task:
            self.cleanup_task.cancel()
            try:
                await self.cleanup_task
            except asyncio.CancelledError:
                pass
            self.cleanup_task = None
        
        logger.info("文件清理服务已停止")

    async def _cleanup_loop(self):
        """清理循环"""
        while self.running:
            try:
                # 等待指定时间间隔
                await asyncio.sleep(self.check_interval_seconds)
                
                # 执行清理
                if self.running:  # 再次检查，避免在等待期间服务被停止
                    await self.cleanup_old_files()
            except asyncio.CancelledError:
                break
            except Exception as e:
                logger.error(f"文件清理过程中发生错误: {str(e)}")
                await asyncio.sleep(self.check_interval_seconds)

    async def cleanup_old_files(self):
        """清理旧文件"""
        cutoff_time = time.time() - (self.max_age_minutes * 60)
        
        # 清理输出目录
        await self._cleanup_directory(self.output_dir, cutoff_time)
        
        # 清理临时目录
        await self._cleanup_directory(self.temp_dir, cutoff_time)
        
        # 清理上传目录
        await self._cleanup_directory(self.uploads_dir, cutoff_time)

    async def manual_cleanup(self, max_age_minutes: Optional[int] = None) -> dict:
        """手动触发清理操作
        
        Args:
            max_age_minutes: 可选的临时最大保留时间（分钟），不提供则使用默认值
            
        Returns:
            清理结果统计
        """
        # 如果提供了临时最大保留时间，则临时覆盖类属性
        original_max_age = self.max_age_minutes
        if max_age_minutes is not None:
            self.max_age_minutes = max_age_minutes
        
        start_time = time.time()
        
        try:
            # 记录原始状态
            stats_before = await self._get_directory_stats()
            
            # 执行清理
            await self.cleanup_old_files()
            
            # 记录清理后状态
            stats_after = await self._get_directory_stats()
            
            # 计算清理结果
            result = {
                "cleaned_files_count": {
                    "output": stats_before["output"]["files"] - stats_after["output"]["files"],
                    "temp": stats_before["temp"]["files"] - stats_after["temp"]["files"],
                    "uploads": stats_before["uploads"]["files"] - stats_after["uploads"]["files"],
                },
                "cleaned_dirs_count": {
                    "output": stats_before["output"]["dirs"] - stats_after["output"]["dirs"],
                    "temp": stats_before["temp"]["dirs"] - stats_after["temp"]["dirs"],
                    "uploads": stats_before["uploads"]["dirs"] - stats_after["uploads"]["dirs"],
                },
                "time_taken_seconds": round(time.time() - start_time, 2),
                "max_age_minutes": self.max_age_minutes,
                "timestamp": datetime.now().isoformat()
            }
            
            return result
        finally:
            # 恢复原始最大保留时间
            if max_age_minutes is not None:
                self.max_age_minutes = original_max_age

    async def _get_directory_stats(self) -> dict:
        """获取目录统计信息
        
        Returns:
            包含各目录文件和子目录数量的字典
        """
        return {
            "output": await self._count_files_and_dirs(self.output_dir),
            "temp": await self._count_files_and_dirs(self.temp_dir),
            "uploads": await self._count_files_and_dirs(self.uploads_dir),
        }
    
    async def _count_files_and_dirs(self, directory: Path) -> dict:
        """计算目录中的文件和子目录数量
        
        Args:
            directory: 要统计的目录
            
        Returns:
            包含文件和目录数量的字典
        """
        if not directory.exists():
            return {"files": 0, "dirs": 0}
        
        try:
            files_count = 0
            dirs_count = 0
            
            for item in directory.glob("**/*"):
                if item.is_file():
                    files_count += 1
                elif item.is_dir():
                    dirs_count += 1
            
            return {
                "files": files_count,
                "dirs": dirs_count
            }
        except Exception as e:
            logger.error(f"统计目录 {directory} 失败: {str(e)}")
            return {"files": 0, "dirs": 0}

    async def _cleanup_directory(self, directory: Path, cutoff_time: float):
        """清理指定目录下的旧文件
        
        Args:
            directory: 要清理的目录
            cutoff_time: 截止时间（Unix时间戳）
        """
        if not directory.exists():
            logger.warning(f"目录不存在: {directory}")
            return
        
        try:
            deleted_count = 0
            skipped_count = 0
            
            # 第一步：先收集所有过期文件
            expired_files = []
            for item in directory.glob("**/*"):
                if not item.exists():
                    continue
                    
                try:
                    # 获取文件修改时间
                    mod_time = item.stat().st_mtime
                    
                    # 如果是文件且修改时间早于截止时间，则添加到删除列表
                    if item.is_file() and mod_time < cutoff_time:
                        expired_files.append(item)
                except Exception as e:
                    logger.error(f"获取文件状态失败 {item}: {str(e)}")
            
            # 第二步：删除所有过期文件
            for file_path in expired_files:
                success = await self._try_delete_file(file_path)
                if success:
                    deleted_count += 1
                else:
                    skipped_count += 1
            
            # 第三步：多次尝试清理空目录（自底向上）
            for _ in range(3):  # 尝试多次，以处理嵌套目录
                deleted_dirs = await self._cleanup_empty_dirs(directory)
                deleted_count += deleted_dirs
                
                # 如果没有删除任何目录，则不必继续尝试
                if deleted_dirs == 0:
                    break
                    
                # 在尝试之间添加短暂延迟，让系统有时间处理
                await asyncio.sleep(0.5)
            
            # 记录清理结果
            if deleted_count > 0 or skipped_count > 0:
                log_msg = f"从 {directory} 目录清理了 {deleted_count} 个过期项目"
                if skipped_count > 0:
                    log_msg += f"，跳过了 {skipped_count} 个无法删除的项目"
                logger.info(log_msg)
        
        except Exception as e:
            logger.error(f"清理目录 {directory} 时发生错误: {str(e)}")

    async def _try_delete_file(self, file_path: Path) -> bool:
        """尝试删除文件，处理各种可能的错误
        
        Args:
            file_path: 文件路径
            
        Returns:
            bool: 删除成功返回True，否则返回False
        """
        if not file_path.exists():
            return True
            
        # 尝试普通删除
        try:
            file_path.unlink(missing_ok=True)
            return True
        except PermissionError:
            pass  # 继续尝试其他方法
        except Exception as e:
            logger.error(f"删除文件失败 {file_path}: {str(e)}")
            
        # Windows平台特殊处理
        if self.is_windows:
            try:
                # 尝试使用Windows API解除文件锁定
                if self._unlock_windows_file(file_path):
                    try:
                        file_path.unlink(missing_ok=True)
                        return True
                    except:
                        pass
                        
                # 尝试使用os.remove
                os.remove(str(file_path))
                return True
            except:
                # 尝试使用cmd强制删除
                return await self._force_delete_windows_file(file_path)
        
        # 非Windows平台或所有方法都失败
        return False
        
    def _unlock_windows_file(self, file_path: Path) -> bool:
        """尝试解除Windows文件锁定
        
        Args:
            file_path: 文件路径
            
        Returns:
            bool: 成功返回True，否则返回False
        """
        if not self.is_windows:
            return False
            
        try:
            # 使用Windows API尝试解除文件锁定
            # 这只是一个示例，可能不适用于所有情况
            file_path_str = str(file_path)
            
            # 先重命名文件，有时这能解除锁定
            temp_name = f"{file_path_str}.tmp"
            try:
                os.rename(file_path_str, temp_name)
                os.rename(temp_name, file_path_str)
                return True
            except:
                pass
                
            return False
        except:
            return False
    
    async def _force_delete_windows_file(self, file_path: Path) -> bool:
        """使用Windows命令行强制删除文件
        
        Args:
            file_path: 文件路径
            
        Returns:
            bool: 成功返回True，否则返回False
        """
        if not self.is_windows:
            return False
            
        try:
            # 使用DEL /F /Q 命令强制删除文件
            file_path_str = str(file_path).replace('/', '\\')
            
            # 在异步环境中运行同步命令
            process = await asyncio.create_subprocess_shell(
                f'DEL /F /Q "{file_path_str}"',
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE
            )
            
            # 等待命令完成
            await process.communicate()
            
            # 检查文件是否仍然存在
            return not file_path.exists()
        except Exception as e:
            logger.debug(f"强制删除文件失败 {file_path}: {str(e)}")
            return False

    async def _cleanup_empty_dirs(self, directory: Path) -> int:
        """清理空目录
        
        Args:
            directory: 要清理的目录
            
        Returns:
            int: 删除的目录数量
        """
        if not directory.exists():
            return 0
            
        deleted_count = 0
        
        try:
            # 自底向上收集所有看似空的目录
            empty_dirs = []
            for root, dirs, files in os.walk(directory, topdown=False):
                if root == str(directory):
                    continue  # 跳过基本目录
                    
                root_path = Path(root)
                
                # 如果目录中没有文件且看上去为空
                if not files and self._is_dir_effectively_empty(root_path):
                    empty_dirs.append(root_path)
            
            # 按照深度排序，先删除最深的目录
            empty_dirs.sort(key=lambda p: len(str(p).split(os.sep)), reverse=True)
            
            # 尝试删除这些目录
            for dir_path in empty_dirs:
                if not dir_path.exists():
                    continue
                    
                success = await self._try_delete_directory(dir_path)
                if success:
                    deleted_count += 1
        except Exception as e:
            logger.error(f"清理空目录时发生错误: {str(e)}")
                
        return deleted_count
    
    async def _try_delete_directory(self, dir_path: Path) -> bool:
        """尝试删除目录，处理各种可能的错误
        
        Args:
            dir_path: 目录路径
            
        Returns:
            bool: 删除成功返回True，否则返回False
        """
        if not dir_path.exists():
            return True
            
        # 检查目录是否实际为空
        if not self._is_dir_effectively_empty(dir_path):
            # 先尝试删除可能存在的隐藏文件
            for item in dir_path.iterdir():
                try:
                    if item.is_file():
                        await self._try_delete_file(item)
                    elif item.is_dir() and self._is_dir_effectively_empty(item):
                        await self._try_delete_directory(item)
                except Exception:
                    pass
        
        # 尝试普通删除
        try:
            dir_path.rmdir()
            return True
        except OSError as e:
            if "目录不是空的" in str(e) or "directory not empty" in str(e).lower():
                # 如果目录应该为空但仍报错，可能是Windows的特殊情况
                pass
            else:
                logger.error(f"删除目录失败 {dir_path}: {str(e)}")
                return False
        except Exception as e:
            logger.error(f"删除目录失败 {dir_path}: {str(e)}")
            return False
        
        # Windows平台特殊处理
        if self.is_windows:
            try:
                # 尝试使用shutil.rmtree强制删除
                shutil.rmtree(dir_path, ignore_errors=True)
                return not dir_path.exists()
            except:
                pass
                
            # 尝试使用命令行强制删除
            return await self._force_delete_windows_directory(dir_path)
        
        return False
    
    async def _force_delete_windows_directory(self, dir_path: Path) -> bool:
        """使用Windows命令行强制删除目录
        
        Args:
            dir_path: 目录路径
            
        Returns:
            bool: 成功返回True，否则返回False
        """
        if not self.is_windows:
            return False
            
        try:
            dir_path_str = str(dir_path).replace('/', '\\')
            
            # 尝试使用RMDIR /S /Q命令强制删除目录
            process = await asyncio.create_subprocess_shell(
                f'RMDIR /S /Q "{dir_path_str}"',
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE
            )
            
            # 等待命令完成
            await process.communicate()
            
            # 检查目录是否仍然存在
            return not dir_path.exists()
        except Exception as e:
            logger.debug(f"强制删除目录失败 {dir_path}: {str(e)}")
            return False
    
    @staticmethod
    def _is_dir_effectively_empty(dir_path: Path) -> bool:
        """检查目录是否实际上为空（排除Windows特殊文件）
        
        Args:
            dir_path: 目录路径
            
        Returns:
            bool: 如果目录实际为空则返回True
        """
        if not dir_path.exists():
            return True
            
        try:
            # 检查目录中的所有项目
            items = list(dir_path.iterdir())
            
            # 如果没有项目，则目录为空
            if not items:
                return True
                
            # 检查是否只有Windows特殊文件
            for item in items:
                # 跳过Windows缩略图缓存文件
                if item.name.lower() == "thumbs.db":
                    continue
                # 跳过Windows索引文件
                if item.name.lower() == "desktop.ini":
                    continue
                # 跳过Windows回收站文件
                if item.name.lower() == "$recycle.bin":
                    continue
                # 如果还有其他文件，则目录不为空
                return False
                
            # 如果只有可以忽略的特殊文件，也认为目录为空
            return True
        except Exception:
            # 如果出现错误，保守地认为目录不为空
            return False

    @staticmethod
    def format_time_delta(seconds: float) -> str:
        """格式化时间差
        
        Args:
            seconds: 秒数
            
        Returns:
            格式化后的时间字符串
        """
        minutes, seconds = divmod(int(seconds), 60)
        hours, minutes = divmod(minutes, 60)
        days, hours = divmod(hours, 24)
        
        parts = []
        if days > 0:
            parts.append(f"{days}天")
        if hours > 0:
            parts.append(f"{hours}小时")
        if minutes > 0:
            parts.append(f"{minutes}分钟")
        if seconds > 0 or not parts:
            parts.append(f"{seconds}秒")
            
        return " ".join(parts) 